<?php
/**
 * db.class
 * 
 * Singleton database wrapper
 * 
 * @version:	0.5
 * @package:	core
 * @author:	alex_ez
 * @created:	11 nov 2008
 *
 * @link:	http://src.qfox.ru/
 *
 * @copyright:	(c) 2008 flamebox.ru. All rights reserved.
 */

if( defined( '__DB.CLASS_PHP' ) ) return;
define( '__DB.CLASS_PHP', '0.5' );

class db extends singleton
{
	// php singleton wrapper
	static public function &__getMe( ) { return parent::__getMe( __CLASS__ ); }
	static public function &instance() { return parent::__getMe(__CLASS__); }
	
	static private $link = null;
	
	static private $stats = null;
	
	private $lastQuery = null;
	/**
	 * Конструктор
	 *
	 * @access protected
	 */
	protected function __construct( )
	{
		if( !defined( 'DB_URL' ) ) return;
		
		$z = parse_url( DB_URL );
		if( $z['scheme'] !== 'mysqli' )
			return null;
		
		list(, $_db ) = explode( '/', $z['path'] );
		
		$_params = array( 'charset' => 'utf8', );
		parse_str( $z['query'], $_query ); $z['query'] = $_query; unset( $_query );
		foreach( $z['query'] as $k => $v ) $_params[ $k ] = $v;
		
		self::$link = new mysqli(
			$z['host'],
			$z['user'],
			$z['pass'],
			$_db,
			$z['port']
			);
		
		if( $_e = mysqli_connect_errno() ) {
			throw new Exception( sprintf( "Connect failed: %s\n", $_e ) );
		}
                
		if( !is_array( self::$stats ) )
			self::$stats = array( 'queriesCount' => 0, 'queriesTime' => 0, 'execsCount' => 0, 'execsTime' => 0 );
		
		self::$link ->set_charset( $_params['charset'] );
	}
	
	/** 
	 * killer
	 *
	 * @access public
	 */
	public function __destruct( )
	{
		if( is_object( self::$link ) ) self::$link ->close( );
		self::$link = null;
	}



	/**
	 * mysqli methods wrapper
	 *
	 * @access public
	 * @param string $name имя метода
	 * @param array $params массив аргументов (в коде не нужно передавать массив)
	 * @return mixed
	 */
	public function __call( $name, $params )
	{
		if( method_exists( self::$link, $name ) )
			return call_user_func_array( array( self::$link, $name ), $params );
		
		throw new Exception( 'Method "'.$name.'" of class "'.__CLASS__.'" does not exists' );
	}

	/**
	 * accessing to properties of encapsulated mysqli-object
	 *
	 * @param	string	property name of encapsulated mysqli-object
	 * @return	mixed	value, if exists, or null
	 * @date	27 august 2008
	 * @author	alex yaroshevich
	 */
	public function __get( $property )
	{
		return self::$link ->$property;
	}
	
	public function exec( $q )
	{
		fb::group( 'exec['.self::$stats['execsCount'].']'/*, array( 'Collapsed'=>false, 'Color' => '#941' )*/ );
		$_z = microtime(1);
		$res = self::$link ->query( $q );
		$this ->lastQuery = $q;
		fb::log( $q, 'query' );
		if( $res === false )
			fb::error( self::$link ->error, 'error' );
		self::$stats['execsTime'] += microtime(1)-$_z;
		self::$stats['execsCount'] ++;
		fb::log( sprintf( '%9.4fms', 1000*(microtime(1)-$_z) ), 'elapsed' );
		$r = ( self::$link ->affected_rows != 1 || !self::$link ->insert_id ) ? self::$link ->affected_rows : self::$link ->insert_id;
		fb::log( $r, 'result' );
		fb::groupEnd( );
		return $r;
	}
	
	/**
	 * query
	 *
	 * @access public
	 * @param string $query строка запроса
	 * @return mysqli_result результат выполнения запроса
	 */
	public function query( $q )
	{
		fb::group( 'query['.self::$stats['queriesCount'].']'/*, array( 'Collapsed'=>false, 'Color' => '#194' )*/ );
		$_z = microtime(1);
		$res = self::$link ->query( $q );
		$this ->lastQuery = $q;
		fb::log( $q, 'query' );
		if( $res === false )
		{
			fb::error( self::$link ->error, 'error' );
			fb::groupEnd( );
			throw new Exception( self::$link ->error );
		}
		self::$stats['queriesTime'] += microtime(1)-$_z;
		self::$stats['queriesCount'] ++;
		fb::log( sprintf( '%9.4fms', 1000*(microtime(1)-$_z) ), 'elapsed' );
		//if( array_key_exists( 'id', $res ) ) fb::log( $res, 'result' ); else { fb::log( reset( $res ), 'row[0]' ); fb::log( count( $res ), 'rows' ); }
		fb::groupEnd( );
		return $res;
	}
	
	public function _value( $q )
	{
		$_r = $this ->_row( $q );
		return is_array( $_r ) ? reset( $_r ) : null;
	}
	
	public function _row( $q )
	{
		$result = $this ->query( $q );
		$_r = $result ->fetch_assoc( );
		$result ->free_result( );
		return $_r;
	}
	
	public function _list( $q, $k = null, $kv = null )
	{
		$_r = array( );
		
		$result = $this ->query( $q );
		
		$z = $result ->fetch_assoc( );
		$k = ( !is_null($k) && isset($z[$k]) ) ? $k : null;
		$kv = ( !is_null($kv) && isset($z[$kv]) ) ? $kv : null;
		
		if( $z )
		{
		do {
			$_v = !is_null( $kv ) ? $z[ $kv ] : $z;
			if( !is_null( $k ) ) $_r[ $z[$k] ] = $_v; else $_r[ ] = $_v;
		} while( $z = $result ->fetch_assoc( ) );
		}
		
		$result ->free_result( );
		
		return $_r;
	}
	
	public function esc( $v ) { return $this ->escape( $v ); }
	public function escape( $v )
	{
		if( !is_array( $v ) ) return self::$link ->escape_string( $v );
		foreach( array_keys( $v ) as $k ) $v[$k] = self::$link ->escape_string( $v[$k] ); return $v;
	}
	
	
	/**
	 * Utilites
	 *
	 */
	public function makeSetList( $a, $d = ',' )
	{
		if( !is_array( $a ) ) return;
		
		//fb::group('db::makeSetList',array('Collapsed'=>true,'Color'=>'#39f'));
		$d = (string)($d);
		$r = '';
		
		//fb::log( $a, 'in: '.$d );
		
		foreach( $a as $k => $v )
			$r .= $this ->makeSetListItem( $k, $v, $d );
		$r = mb_substr( $r, 0, -mb_strlen($d.'  ') );
		
		//fb::log( $r, 'out' );
		//fb::log( $r, 'result' );
		//fb::groupEnd( );
		return $r;
	}

	public function makeSetListItem( $k, $v, $d = ',' )
	{
		// $d == ',' is update/insert query or anything important-less. else is a condition like A OR B OR C, or like A AND B AND C
		$mode = ($d==',') ? 'list' : 'condition';
		$op = substr( $k, -1, 1 ); $op = strpos( '~/<>^!+-@', $op ) === false ? '=' : $op;
		$k = rtrim( $k, $op );

		//fb::log( array( $k, $v, $mode, $op ), 'db.it' );
		
		if( $mode == 'list' )
		{
		return
			(is_null($v)		? sprintf( "`%s` = null", $k )
			:(is_int($v) ?
				($op === '='	? sprintf( '`%s` = %d', $k, $v )
								: sprintf( '`%1$s` = %1$s %2$s %3$d', $k, $op, $v ))
			:(($op=='+')		? sprintf( '`%1$s` = concat( %1$s, "%2$s")', $k, $this ->escape( is_array( $v ) ? join( ',', $v ) : $v ) )
					: sprintf( "`%s` = '%s'", $k, $this ->escape( is_array( $v ) ? join( ',', $v ) : $v ) )
			)))." {$d} ";
		}
		
		return
			(($op==='~')	? sprintf( '`%s` like "%3$s%2$s%3$s" ', $k, $this ->escape( $v ), strpos( $v, '%' ) !== false ? '' : '%' )
			:(($op==='/')	? sprintf( "`%s` regexp '%s' ", $k, $this ->escape( $v ) )
			:(is_null($v)	? sprintf( 'isnull( `%s` )', $k )
			:(($op==='^')	? sprintf( "`%s` between '%s' AND '%s'", $k, $this ->escape( $v[0] ), $this ->escape( $v[1] ) )
			:(($op==='!' || $op==='+' || $op==='-')
							? sprintf( '`%s` %s= "%s" ', $k, $op, $this ->escape( $v ) )
			:(($op==='@')	? sprintf( '`%s` in (%s) ', $k, $v )
			:(is_array($v)	? sprintf( "`%s` in (%s) ", $k, '"'.join( '", "', $this ->escape( $v ) ).'"' )
					: sprintf( '`%s` %s "%s" ', $k, $op, $this ->escape( $v ) )
			)))))))." {$d} ";
	}

	public function makeInsertValuesKeys( $a )
	{
		$keys = array( );
		foreach( array_keys( $a ) as $k )
			$keys = array_merge( $keys, array_diff( array_keys( $a[$k] ), $keys ) );
		return $keys;
	}
	
	public function makeInsertValuesList( $a, $emptyRow = null )
	{
		if( !is_array( $a ) ) return;
		
		// prepare $emptyRow if null
		if( !is_array( $emptyRow ) )
		{
			$keys = $this ->makeInsertValuesKeys( $a );
			$emptyRow = array_fill_keys( $keys, null );
		}
		
		// prepare rows
		$rows = array( );
		foreach( $a as $row )
		{
			$newRow = $emptyRow;
			foreach( array_keys( $emptyRow ) as $k )
			{
				$v = _::pick( @$row[$k], $emptyRow[$k] );
				$newRow[$k] =
					(  is_null($v) ? "null"
					:( is_int($v) ? intval( $v )
					:( '"'.($this ->escape( is_array( $v ) ? join( ',', $v ) : $v )).'"'
					)));
			}
			$rows[] = $newRow;
		}
		
		$r = '';
		foreach( $rows as $row )
			$r .= '( '.join( ', ', $row ).' ), ';
		$r = substr( $r, 0, -2 );
		
		return $r;
	}

	public function makeInsertValuesQuery( $table, $a, $emptyRow = null, $ignore = null )
	{
		if( is_null( $emptyRow ) || !is_array( $emptyRow ) )
		{
			$keys = $this ->makeInsertValuesKeys( $a );
			$emptyRow = array_fill_keys( $keys, null );
		} else
			$keys = array_keys( $emptyRow );
		
		$keysList = '`'.join( '`, `', $keys ).'`';
		$valuesList = $this ->makeInsertValuesList( $a, $emptyRow );
		$operator = $ignore?($ignore==1?"INSERT IGNORE":"REPLACE"):"INSERT";
		
		$q = "%s INTO %s(%s) VALUES %s;";
		$q = sprintf( $q, $operator, $table, $keysList, $valuesList );

		return $q;
	}






	
	public function getStats( $fullTime = false )
	{
		if( $fullTime )
			return self::$stats['queriesTime'] + self::$stats['execsTime'];
		
		if( self::$stats['queriesCount'] == 0 && self::$stats['execsCount'] == 0 )
			return '0';
		
		if( self::$stats['execsCount'] == 0 )
			return sprintf( "%.3f<sup>%d</sup>", self::$stats['queriesTime'], self::$stats['queriesCount'] );
		
		return sprintf( "%.3f<sup>%d</sup> [ %.3f<sup>%d</sup> + %.3f<sup>%d</sup> ]",
			self::$stats['queriesTime'] + self::$stats['execsTime'],
			self::$stats['queriesCount'] + self::$stats['execsCount'],
			self::$stats['queriesTime'], self::$stats['queriesCount'],
			self::$stats['execsTime'], self::$stats['execsCount']
			);
	}
	
	public function lastQuery( ) { return $this ->lastQuery; }
}

?>